/**
 * Copyright (c) 2020 lizebang
 *
 * This software is released under the MIT License.
 * https://opensource.org/licenses/MIT
 */

package jiri

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"time"

	"gitee.com/banggua/ggtsync/logger"
	"go.fuchsia.dev/jiri/project"
)

const (
	cipdEndpoint = "https://chrome-infra-packages.appspot.com/dl/%s/+/%s"
)

type PackageState struct {
	PackagePath    string
	LatestUsedTime time.Time
}

func packageLocalPath(path, name, version string) string {
	return filepath.Join(path, name+":"+version)
}

func packageRemoteURL(name, version string) string {
	return fmt.Sprintf(cipdEndpoint, name, version)
}

type PackageStates map[string]PackageState

func getPackageStates(to string, packages project.Packages) PackageStates {
	packageStates := make(PackageStates)
	for _, pkg := range packages {
		if len(pkg.Instances) > 0 {
			for _, instance := range pkg.Instances {
				packageStates[packageLocalPath(to, instance.Name, instance.ID)] = PackageState{LatestUsedTime: time.Now(), PackagePath: packageLocalPath(to, instance.Name, instance.ID)}
			}
		} else {
			packageStates[packageLocalPath(to, pkg.Name, pkg.Version)] = PackageState{LatestUsedTime: time.Now(), PackagePath: packageLocalPath(to, pkg.Name, pkg.Version)}
		}
	}
	return packageStates
}

var (
	packageStatesLock = "package-states.lock"
)

func loadPackageStates(path string) (PackageStates, error) {
	var packageStates = make(PackageStates)

	packageStatesFile, err := os.OpenFile(filepath.Join(path, packageStatesLock), os.O_RDONLY, 0644)
	if err != nil {
		if os.IsNotExist(err) {
			return packageStates, nil
		}
		logger.Errorf("%s %v", path, err)
		return nil, err
	}

	packageStatesJSON, err := ioutil.ReadAll(packageStatesFile)
	if err != nil {
		logger.Errorf("%s %v", path, err)
		return nil, err
	}

	err = json.Unmarshal(packageStatesJSON, &packageStates)
	if err != nil {
		os.Remove(filepath.Join(path, packageStatesLock))
		logger.Errorf("%s %v", path, err)
		return nil, err
	}

	return packageStates, nil
}

func lockPackageStates(path string, packageStates PackageStates) error {
	packageStatesFile, err := os.OpenFile(filepath.Join(path, packageStatesLock), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		logger.Errorf("%s %v %v", path, packageStates, err)
		return err
	}
	defer packageStatesFile.Close()

	packageStatesJSON, err := json.MarshalIndent(&packageStates, "", "\t")
	if err != nil {
		logger.Errorf("%s %v %v", path, packageStates, err)
		return err
	}

	_, err = packageStatesFile.Write(packageStatesJSON)
	if err != nil {
		logger.Errorf("%s %v %v", path, packageStates, err)
		return err
	}

	return nil
}

func computePackages(to string, localPackageStates PackageStates, remotePackages project.Packages) map[string]operation {
	var operations = make(map[string]operation)

	for _, remotePackage := range remotePackages {
		if remotePackage.Internal {
			continue
		}
		if len(remotePackage.Instances) > 0 {
			for _, instance := range remotePackage.Instances {
				path := packageLocalPath(to, instance.Name, instance.ID)
				remote := packageRemoteURL(instance.Name, instance.ID)
				_, err := os.Stat(path)
				if localPackageState, isExist := localPackageStates[path]; isExist && !os.IsNotExist(err) {
					localPackageState.LatestUsedTime = time.Now()
				} else {
					localPackageStates[path] = PackageState{LatestUsedTime: time.Now(), PackagePath: path}
					operations[path] = &createOperation{optype: packageOp, path: path, remote: remote}
				}
			}
		} else {
			path := packageLocalPath(to, remotePackage.Name, remotePackage.Version)
			remote := packageRemoteURL(remotePackage.Name, remotePackage.Version)
			_, err := os.Stat(path)
			if localPackageState, isExist := localPackageStates[path]; isExist && !os.IsNotExist(err) {
				localPackageState.LatestUsedTime = time.Now()
			} else {
				localPackageStates[path] = PackageState{LatestUsedTime: time.Now(), PackagePath: path}
				operations[path] = &createOperation{optype: packageOp, path: path, remote: remote}
			}
		}
	}

	for localPackageStateKey, localPackageState := range localPackageStates {
		if _, isExist := operations[localPackageState.PackagePath]; !isExist {
			if time.Now().Sub(localPackageState.LatestUsedTime).Hours() > 7*24 {
				delete(localPackageStates, localPackageStateKey)
				operations[localPackageState.PackagePath] = &deleteOperation{path: localPackageState.PackagePath}
			}
		}
	}

	return operations
}
